package com.swak.fx.support.ui;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import com.swak.fx.support.transition.UIAnimationTimer;
import com.swak.fx.support.transition.UIKeyFrame;
import com.swak.fx.support.transition.UIKeyValue;

import javafx.animation.Interpolator;
import javafx.css.CssMetaData;
import javafx.css.SimpleStyleableBooleanProperty;
import javafx.css.SimpleStyleableObjectProperty;
import javafx.css.Styleable;
import javafx.css.StyleableBooleanProperty;
import javafx.css.StyleableObjectProperty;
import javafx.css.StyleableProperty;
import javafx.css.converter.BooleanConverter;
import javafx.css.converter.ColorConverter;
import javafx.geometry.HPos;
import javafx.geometry.VPos;
import javafx.scene.control.RadioButton;
import javafx.scene.control.Skin;
import javafx.scene.control.skin.RadioButtonSkin;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.util.Duration;

public class UIRadioButton extends RadioButton implements UILifecycle {

	// ================================================================================
	// Properties
	// ================================================================================
	private final String STYLE_CLASS = "ui-radio";
	private final String STYLESHEET = UI.load("css/UIRadio.css");

	public UIRadioButton() {
		super();
		init();
	}

	public UIRadioButton(String text) {
		super(text);
		init();
	}

	@Override
	public void initializeParts() {
		getStyleClass().addAll(STYLE_CLASS);
	}

	@Override
	public void layoutParts() {
	}

	@Override
	public void setupValueChangedListeners() {

	}

	// ================================================================================
	// Overridden Methods
	// ================================================================================

	@Override
	public String getUserAgentStylesheet() {
		return STYLESHEET;
	}

	/**
	* default color used when the radio button is selected
	*/
	private StyleableObjectProperty<Color> selectedColor = new SimpleStyleableObjectProperty<>(
			StyleableProperties.SELECTED_COLOR, UIRadioButton.this, "selectedColor", Color.valueOf("#0F9D58"));

	public final StyleableObjectProperty<Color> selectedColorProperty() {
		return this.selectedColor;
	}

	public final Color getSelectedColor() {
		return selectedColor == null ? Color.rgb(0, 0, 0, 0.2) : this.selectedColorProperty().get();
	}

	public final void setSelectedColor(final Color selectedColor) {
		this.selectedColorProperty().set(selectedColor);
	}

	/**
	 * default color used when the radio button is not selected
	 */
	private StyleableObjectProperty<Color> unSelectedColor = new SimpleStyleableObjectProperty<>(
			StyleableProperties.UNSELECTED_COLOR, UIRadioButton.this, "unSelectedColor", Color.valueOf("#5A5A5A"));

	public final StyleableObjectProperty<Color> unSelectedColorProperty() {
		return this.unSelectedColor;
	}

	public final Color getUnSelectedColor() {
		return unSelectedColor == null ? Color.TRANSPARENT : this.unSelectedColorProperty().get();
	}

	public final void setUnSelectedColor(final Color unSelectedColor) {
		this.unSelectedColorProperty().set(unSelectedColor);
	}

	/**
	 * Disable the visual indicator for focus
	 */
	private StyleableBooleanProperty disableVisualFocus = new SimpleStyleableBooleanProperty(
			StyleableProperties.DISABLE_VISUAL_FOCUS, UIRadioButton.this, "disableVisualFocus", false);

	public final StyleableBooleanProperty disableVisualFocusProperty() {
		return this.disableVisualFocus;
	}

	public final Boolean isDisableVisualFocus() {
		return disableVisualFocus != null && this.disableVisualFocusProperty().get();
	}

	public final void setDisableVisualFocus(final Boolean disabled) {
		this.disableVisualFocusProperty().set(disabled);
	}

	/**
	 * disable animation on button action
	 */
	private StyleableBooleanProperty disableAnimation = new SimpleStyleableBooleanProperty(
			UIRadioButton.StyleableProperties.DISABLE_ANIMATION, UIRadioButton.this, "disableAnimation", false);

	public final StyleableBooleanProperty disableAnimationProperty() {
		return this.disableAnimation;
	}

	public final Boolean isDisableAnimation() {
		return disableAnimation != null && this.disableAnimationProperty().get();
	}

	public final void setDisableAnimation(final Boolean disabled) {
		this.disableAnimationProperty().set(disabled);
	}

	private static class StyleableProperties {
		private static final CssMetaData<UIRadioButton, Color> SELECTED_COLOR = new CssMetaData<UIRadioButton, Color>(
				"-ui-selected-color", ColorConverter.getInstance(), Color.valueOf("#0F9D58")) {
			@Override
			public boolean isSettable(UIRadioButton control) {
				return control.selectedColor == null || !control.selectedColor.isBound();
			}

			@Override
			public StyleableProperty<Color> getStyleableProperty(UIRadioButton control) {
				return control.selectedColorProperty();
			}
		};
		private static final CssMetaData<UIRadioButton, Color> UNSELECTED_COLOR = new CssMetaData<UIRadioButton, Color>(
				"-ui-unselected-color", ColorConverter.getInstance(), Color.valueOf("#5A5A5A")) {
			@Override
			public boolean isSettable(UIRadioButton control) {
				return control.unSelectedColor == null || !control.unSelectedColor.isBound();
			}

			@Override
			public StyleableProperty<Color> getStyleableProperty(UIRadioButton control) {
				return control.unSelectedColorProperty();
			}
		};
		private static final CssMetaData<UIRadioButton, Boolean> DISABLE_VISUAL_FOCUS = new CssMetaData<UIRadioButton, Boolean>(
				"-ui-disable-visual-focus", BooleanConverter.getInstance(), false) {
			@Override
			public boolean isSettable(UIRadioButton control) {
				return control.disableVisualFocus == null || !control.disableVisualFocus.isBound();
			}

			@Override
			public StyleableBooleanProperty getStyleableProperty(UIRadioButton control) {
				return control.disableVisualFocusProperty();
			}
		};

		private static final CssMetaData<UIRadioButton, Boolean> DISABLE_ANIMATION = new CssMetaData<UIRadioButton, Boolean>(
				"-ui-disable-animation", BooleanConverter.getInstance(), false) {
			@Override
			public boolean isSettable(UIRadioButton control) {
				return control.disableAnimation == null || !control.disableAnimation.isBound();
			}

			@Override
			public StyleableBooleanProperty getStyleableProperty(UIRadioButton control) {
				return control.disableAnimationProperty();
			}
		};

		private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;

		static {
			final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(
					RadioButton.getClassCssMetaData());
			Collections.addAll(styleables, SELECTED_COLOR, UNSELECTED_COLOR, DISABLE_VISUAL_FOCUS, DISABLE_ANIMATION);
			CHILD_STYLEABLES = Collections.unmodifiableList(styleables);
		}
	}

	@Override
	public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
		return getClassCssMetaData();
	}

	public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
		return StyleableProperties.CHILD_STYLEABLES;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected Skin<?> createDefaultSkin() {
		return new UIRadioButtonSkin(this);
	}

	public static class UIRadioButtonSkin extends RadioButtonSkin {

		private Circle radio, dot;
		private UIAnimationTimer timer;
		private final StackPane container;

		public UIRadioButtonSkin(UIRadioButton control) {
			super(control);

			final double radioRadius = 8;
			radio = new Circle(radioRadius);
			radio.getStyleClass().setAll("radio");
			radio.setStrokeWidth(2);
			radio.setFill(Color.TRANSPARENT);
			radio.setSmooth(true);

			dot = new Circle(radioRadius);
			dot.getStyleClass().setAll("dot");
			dot.fillProperty().bind(control.selectedColorProperty());
			dot.setScaleX(0);
			dot.setScaleY(0);
			dot.setSmooth(true);

			container = new StackPane(radio, dot);
			container.getStyleClass().add("radio-container");

			updateChildren();

			timer = new UIAnimationTimer(new UIKeyFrame(Duration.millis(200),
					UIKeyValue.builder().setTarget(dot.scaleXProperty())
							.setEndValueSupplier(() -> getSkinnable().isSelected() ? 0.55 : 0)
							.setInterpolator(Interpolator.EASE_BOTH).build(),
					UIKeyValue.builder().setTarget(dot.scaleYProperty())
							.setEndValueSupplier(() -> getSkinnable().isSelected() ? 0.55 : 0)
							.setInterpolator(Interpolator.EASE_BOTH).build(),
					UIKeyValue.builder().setTarget(radio.strokeProperty())
							.setEndValueSupplier(() -> getSkinnable().isSelected()
									? ((UIRadioButton) getSkinnable()).getSelectedColor()
									: ((UIRadioButton) getSkinnable()).getUnSelectedColor())
							.setInterpolator(Interpolator.EASE_BOTH).build()));

			registerChangeListener(control.selectedColorProperty(), obs -> updateColors());
			registerChangeListener(control.unSelectedColorProperty(), obs -> updateColors());
			registerChangeListener(control.selectedProperty(), obs -> {
				if (((UIRadioButton) getSkinnable()).isDisableAnimation()) {
					// apply end values
					timer.applyEndValues();
				} else {
					// play selection animation
					timer.reverseAndContinue();
				}
			});
			updateColors();
			timer.applyEndValues();
		}

		@Override
		protected void updateChildren() {
			super.updateChildren();
			if (radio != null) {
				removeRadio();
				getChildren().addAll(container);
			}
		}

		@Override
		protected void layoutChildren(final double x, final double y, final double w, final double h) {
			final RadioButton radioButton = getSkinnable();
			final double contWidth = snapSizeX(container.prefWidth(-1));
			final double contHeight = snapSizeY(container.prefHeight(-1));
			final double computeWidth = Math.max(radioButton.prefWidth(-1), radioButton.minWidth(-1));
			final double width = snapSizeX(contWidth);
			final double height = snapSizeY(contHeight);

			final double labelWidth = Math.min(computeWidth - contWidth, w - width);
			final double labelHeight = Math.min(radioButton.prefHeight(labelWidth), h);
			final double maxHeight = Math.max(contHeight, labelHeight);
			final double xOffset = computeXOffset(w, labelWidth + contWidth, radioButton.getAlignment().getHpos()) + x;
			final double yOffset = computeYOffset(h, maxHeight, radioButton.getAlignment().getVpos()) + x;

			container.resize(width, height);
			layoutLabelInArea(xOffset + contWidth, yOffset, labelWidth, maxHeight, radioButton.getAlignment());
			positionInArea(container, xOffset, yOffset, contWidth, maxHeight, 0, radioButton.getAlignment().getHpos(),
					radioButton.getAlignment().getVpos());
		}

		private void removeRadio() {
			// TODO: replace with removeIf
			for (int i = 0; i < getChildren().size(); i++) {
				if (getChildren().get(i).getStyleClass().contains("radio")) {
					getChildren().remove(i);
					break;
				}
			}
		}

		private void updateColors() {
			boolean isSelected = getSkinnable().isSelected();
			Color unSelectedColor = ((UIRadioButton) getSkinnable()).getUnSelectedColor();
			Color selectedColor = ((UIRadioButton) getSkinnable()).getSelectedColor();
			radio.setStroke(isSelected ? selectedColor : unSelectedColor);
		}

		@Override
		protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset,
				double leftInset) {
			return super.computeMinWidth(height, topInset, rightInset, bottomInset, leftInset)
					+ snapSizeX(radio.minWidth(-1));
		}

		@Override
		protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset,
				double leftInset) {
			return super.computePrefWidth(height, topInset, rightInset, bottomInset, leftInset)
					+ snapSizeX(radio.prefWidth(-1));
		}

		private static double computeXOffset(double width, double contentWidth, HPos hpos) {
			switch (hpos) {
			case LEFT:
				return 0;
			case CENTER:
				return (width - contentWidth) / 2;
			case RIGHT:
				return width - contentWidth;
			}
			return 0;
		}

		private static double computeYOffset(double height, double contentHeight, VPos vpos) {
			switch (vpos) {
			case TOP:
				return 0;
			case CENTER:
				return (height - contentHeight) / 2;
			case BOTTOM:
				return height - contentHeight;
			default:
				return 0;
			}
		}

		@Override
		public void dispose() {
			super.dispose();
			timer.dispose();
			timer = null;
		}
	}

}
